/**
  client.c - Created by Timothy Morey on 4/5/2012.
*/

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef __WINDOWS__

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <WinSock2.h>
#include <WS2tcpip.h>

#pragma warning(disable:4996)

#else

#define INVALID_SOCKET -1
#define SOCKET_ERROR -1
#include <netdb.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>

#endif

#include "client.h"
#include "communication-queue.h"
#include "err.h"
#include "logger.h"
#include "socket-utils.h"
#include "thread-utils.h"


/*****************************************************************************
 * Local definitions:
 *****************************************************************************/

typedef struct
{
  Client Base;
  CommunicationQueue Queue;
  int ClientThreadAlive;
  int KeepClientAlive;

#ifdef __WINDOWS__
  HANDLE ClientThread;
  DWORD ClientThreadId;
  SOCKET Socket;
#else
  pthread_t ClientThread;
  int Socket;
#endif

} ClientEx;

#ifdef __WINDOWS__
DWORD WINAPI ClientThreadEntryPoint(LPVOID arg);
#else
void* ClientThreadEntryPoint(void* arg);
#endif

int ConnectToServer(ClientEx* clex);
int Disconnect(ClientEx* clex);
int ReceiveBuffer(ClientEx* clex, char* buf, int* len);
int SendBuffer(ClientEx* clex, const char* buf, int len);


/*****************************************************************************
 * Implementations of client.h functions:
 *****************************************************************************/

int ConnectAsyncClient(const char* server, const char* port, Client** client)
{
  int retval = NVN_NOERR;

  if(server && port && client)
  {
    ClientEx* clex = (ClientEx*)malloc(sizeof(ClientEx));

    memset(clex, 0, sizeof(ClientEx));
    InitQueue(&clex->Queue);
    strcpy(clex->Base.ServerName, server);
    strcpy(clex->Base.Port, port);
    clex->KeepClientAlive = 1;
    clex->Base.Async = 1;

    if(NVN_NOERR != ConnectToServer(clex))
    {
      DestroyClient((Client*)clex);
      clex = 0;
      retval = NVN_ECONNFAIL;
    }

    if(clex)
    {
  #ifdef __WINDOWS__

      clex->ClientThread = 
        CreateThread(0, 0, ClientThreadEntryPoint, clex, 0, &clex->ClientThreadId);

      if(!clex->ClientThread)
  #else

      if(0 != pthread_create(&clex->ClientThread, 0, ClientThreadEntryPoint, clex))
  #endif
      {
        WriteMessage(LogLevelError, "Failed to create client thread");
        DestroyClient((Client*)clex);
        clex = 0;
        retval = NVN_ETHREADFAIL;
      }
    }

    *client = (Client*)clex;
  }
  else
  {
    retval = NVN_EINVARGS;
  }

  return retval;
}

int ConnectClient(const char* server, const char* port, Client** client)
{
  int retval = NVN_NOERR;

  if(server && port && client)
  {
    ClientEx* clex = (ClientEx*)malloc(sizeof(ClientEx));
    memset(clex, 0, sizeof(ClientEx));
    InitQueue(&clex->Queue);
    strcpy(clex->Base.ServerName, server);
    strcpy(clex->Base.Port, port);
    clex->KeepClientAlive = 1;
    clex->Base.Async = 0;

    if(NVN_NOERR != ConnectToServer(clex))
    {
      DestroyClient((Client*)clex);
      clex = 0;
      retval = NVN_ECONNFAIL;
    }

    *client = (Client*)clex;
  }
  else
  {
    retval = NVN_EINVARGS;
  }

  return retval;
}

int DestroyClient(Client* client)
{
  int retval = NVN_NOERR;
  int valid = 0;

  if(client)
  {
    ClientEx* clex = (ClientEx*)client;
    clex->KeepClientAlive = 0;
    if(clex->Base.Async && clex->ClientThreadAlive)
    {

#ifdef __WINDOWS__
      WaitForSingleObject(clex->ClientThread, INFINITE);
      CloseHandle(clex->ClientThread);
      clex->ClientThread = 0;
#else
      pthread_join(clex->ClientThread, 0);
#endif

    }

    DestroyQueue(clex->Queue);

    if(NVN_NOERR == ValidSocket(clex->Socket, &valid) && valid)
      Disconnect(clex);

    free(client);
  }

  return retval;
}

int PingServer(const char* addr, const char* port)
{
  int retval = NVN_NOERR;
  Client* client = 0;
  const char* req = "ping";
  int reqlen = 4;
  char rep[32];
  int replen = 32;

  if(addr && port)
  {
    retval = ConnectClient(addr, port, &client);

    if(NVN_NOERR == retval)
    {
      memset(rep, 0, replen);
      retval = SendAndReceive(client, req, reqlen, rep, &replen);
    }

    if(NVN_NOERR == retval)
    {
      if(replen != 4 || 0 != strcmp("ping", rep))
      {
        retval = NVN_EPINGFAIL;
      }
    }

    if(client)
      DestroyClient(client);
  }
  else
  {
    retval = NVN_EINVARGS;
  }

  return retval;
}

int Send(Client* client, const char* buf, int len)
{
  int retval = NVN_NOERR;

  if(client && buf && len > 0)
  {
    Message msg;
    ClientEx* clex = (ClientEx*)client;

    if(clex->Base.Async)
    {
      InitMessage(&msg, "Send");
      msg.DestroyArgs = 1;
      msg.Arguments[0] = malloc(len);
      memcpy(msg.Arguments[0], buf, len);
      msg.Arguments[1] = malloc(sizeof(int));
      *((int*)msg.Arguments[1]) = len;
      Push(&clex->Queue, msg);
    }
    else
    {
      retval = SendBuffer(clex, buf, len);
    }
  }
  else
  {
    retval = NVN_EINVARGS;
  }

  return retval;
}

int SendAndReceive(Client* client, 
                   const char* sendBuf, int sendLen, 
                   char* recvBuf, int* recvLen)
{
  int retval = NVN_NOERR;
  Message msg;
  int handled = 0;

  if(client && sendBuf && sendLen > 0)
  {
    if(recvBuf && recvLen && *recvLen > 0)
    {
      ClientEx* clex = (ClientEx*)client;
      if(clex->Base.Async)
      {
        InitMessage(&msg, "SendAndReceive");
        msg.Arguments[0] = &sendBuf;
        msg.Arguments[1] = &sendLen;
        msg.Arguments[2] = &recvBuf;
        msg.Arguments[3] = &recvLen;
        msg.Handled = &handled;
        msg.Result = &retval;
        msg.ResultSize = sizeof(int);
        Push(&clex->Queue, msg);

        while(! handled)
          ThreadYield();
      }
      else
      {
        retval = SendBuffer(clex, sendBuf, sendLen);
        if(NVN_NOERR == retval)
          retval = ReceiveBuffer(clex, recvBuf, recvLen);
      }
    }
    else
    {
      /* We don't have a receive buffer, so we can't accept a response - just
         do a Send instead. */
      retval = Send(client, sendBuf, sendLen);
    }
  }
  else
  {
    retval = NVN_EINVARGS;
  }

  return retval;
}


/*****************************************************************************
 * Implemenations of local functions:
 *****************************************************************************/

#ifdef __WINDOWS__
DWORD WINAPI ClientThreadEntryPoint(LPVOID arg)
#else
void* ClientThreadEntryPoint(void* arg)
#endif
{
  if(arg)
  {
    ClientEx* clex = (ClientEx*)arg;
    clex->ClientThreadAlive = 1;

    // TODO: this while loop consumes too many cpu resources when the client
    // doesn't have anything to do...
    while(clex->KeepClientAlive || clex->Queue.Size > 0)
    {
      if(clex->Queue.Size > 0)
      {
        Message msg;
        int valid = 0;
        Pop(&clex->Queue, &msg, &valid);
        if(valid)
        {
          if(0 == strcmp("Send", msg.Message))
          {
            int r = SendBuffer(clex,
                               (const char*)msg.Arguments[0],
                               *((int*)msg.Arguments[1]));
          
            if(msg.Result && msg.ResultSize >= sizeof(int))
              *((int*)msg.Result) = r;

            if(msg.Handled)
              *msg.Handled = 1;
          }
          else if(0 == strcmp("SendAndReceive", msg.Message))
          {
            SendBuffer(clex,
                       *((const char**)msg.Arguments[0]),
                       *((int*)msg.Arguments[1]));
            ReceiveBuffer(clex,
                          *((char**)msg.Arguments[2]),
                          *((int**)msg.Arguments[3]));
          
            if(msg.Handled)
              *msg.Handled = 1;
          }

          if(msg.DestroyArgs)
            DestroyArguments(msg);
        }
      }
      else
      {
        ThreadSleep(10);
      }
    }

    Disconnect(clex);
    clex->ClientThreadAlive = 0;
  }

  return 0;
}

int ConnectToServer(ClientEx* clex)
{
  int retval = NVN_NOERR;

  if(clex)
  {
    struct addrinfo hints;
    struct addrinfo* match;
    int res = 0;
    char logmsg[64];

    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    res = getaddrinfo(clex->Base.ServerName, 
                      clex->Base.Port, 
                      &hints, 
                      &match);
    if(0 != res)
    {
      sprintf(logmsg, "getaddrinfo failed with return code: %d", res);
      WriteMessage(LogLevelError, logmsg);
      retval = NVN_ECONNFAIL;
    }

    if(NVN_NOERR == retval)
    {
      clex->Socket = socket(match->ai_family, 
                            match->ai_socktype, 
                            match->ai_protocol);

      if(INVALID_SOCKET == clex->Socket)
      {
        PrintSocketError("Failed to create socket");
        retval = NVN_ECONNFAIL;
      }
    }

    if(NVN_NOERR == retval)
    {
      res = connect(clex->Socket, match->ai_addr, (int)match->ai_addrlen);

      if(0 != res)
      {
        PrintSocketError("Failed to connect socket");

#ifdef __WINDOWS__
        closesocket(clex->Socket);
#else
        close(clex->Socket);
#endif

        clex->Socket = INVALID_SOCKET;
        retval = NVN_ECONNFAIL;
      }
    }
  }
  else
  {
    retval = NVN_EINVARGS;
  }

  return retval;
}

int Disconnect(ClientEx* clex)
{
  int retval = NVN_NOERR;
  int valid = 0;

  if(clex && 
     NVN_NOERR == ValidSocket(clex->Socket, &valid) && valid)
  {

#ifdef __WINDOWS__
    shutdown(clex->Socket, SD_BOTH);
    closesocket(clex->Socket);
#else
    shutdown(clex->Socket, SHUT_RDWR);
    close(clex->Socket);
#endif
  }

  return retval;
}

int ReceiveBuffer(ClientEx* clex, char* buf, int* len)
{
  int retval = NVN_NOERR;

  if(clex && buf && len && *len > 0)
  {
    int received = 0;
    int msgSize = 0;
    int n = 0;
    char logmsg[256];

    WriteMessage(LogLevelDebug, "Receiving message size...");

    if(NVN_NOERR != ReceiveMsgSize(clex->Socket, &msgSize))
    {
      sprintf(logmsg, "Failed to receive message size", msgSize);
      WriteMessage(LogLevelError, logmsg);
      retval = NVN_ECOMMFAIL;
    }

    if(msgSize > *len)
    {
      WriteMessage(LogLevelError, "Message is larger than receive buffer");
      retval = NVN_ESMALLBUF;
    }
    else
    {
      *len = msgSize;
    }

    sprintf(logmsg, "Receiving %d byte response from %s:%s", 
            *len, clex->Base.ServerName, clex->Base.Port);
    WriteMessage(LogLevelInfo, logmsg);

    received = 0;
    while(NVN_NOERR == retval && received < *len)
    {
      n = recv(clex->Socket, buf + received, *len - received, 0);
      if(n <= 0)
      {
        PrintSocketError("recv failed");
        retval = NVN_ECOMMFAIL;
      }
      else
      {
        received += n;
      }
    }
  }
  else
  {
    retval = NVN_EINVARGS;
  }

  return retval;
}

int SendBuffer(ClientEx* clex, const char* buf, int len)
{
  int retval = NVN_NOERR;

  if(clex && buf && len > 0)
  {
    int sent = 0;
    int n = 0;
    char logmsg[64];

    sprintf(logmsg, "Sending %d bytes to %s:%s", 
            len, clex->Base.ServerName, clex->Base.Port);
    WriteMessage(LogLevelInfo, logmsg);

    if(NVN_NOERR != SendMsgSize(clex->Socket, len))
    {
      WriteMessage(LogLevelError, "Failed to send message size");
      retval = NVN_ECOMMFAIL;
    }

    sprintf(logmsg, "Sent message size (%d)", len);
    WriteMessage(LogLevelDebug, logmsg);

    sent = 0;
    while(NVN_NOERR == retval && sent < len)
    {
      n = send(clex->Socket, buf + sent, len - sent, 0);
      if(n <= 0)
      {
        PrintSocketError("Socket error on send");
        retval = NVN_ECOMMFAIL;
      }
      else
      {
        sent += n;
      }
    }

    WriteMessage(LogLevelDebug, "Finished sending message");
  }
  else
  {
    retval = NVN_EINVARGS;
  }

  return retval;
}
